import datetime
import json
import math
import time
import traceback
import urllib

from absl import app, flags

import coin.proto.coin_order_enums_pb2 as coin_order
from coin.strategy.kr_hard_arb import hard_arb_general
from coin.strategy.kr_hard_arb.arb_info import create_arb_info

flags.DEFINE_string('key_file1', '', '')

flags.DEFINE_string('key_file2', '', '')

flags.DEFINE_string('exchange1', 'Exchange1', '')

flags.DEFINE_string('exchange2', 'Exchange2', '')

flags.DEFINE_bool('trade_exchange1', False, '')

flags.DEFINE_bool('trade_exchange2', False, '')

flags.DEFINE_bool('passive_only', True, '')

flags.DEFINE_integer('sleep_time', 1, 'sleep time')

flags.DEFINE_string("basis_server_url", "http://52.79.97.52:8080", "")

die_flag = False


def edge_sell1_buy2(bid1, ask2):
  return (float(bid1) / float(ask2) - 1)


def edge_buy1_sell2(ask1, bid2):
  return (float(bid2) / float(ask1) - 1)


def get_basis_json(basis_server_url=None):
  if basis_server_url is None:
    basis_server_url = flags.FLAGS.basis_server_url

  data = json.load(urllib.request.urlopen(basis_server_url))
  return data


def reverse_basis(basis_value):
  return 1.0 / (1.0 + basis_value) - 1.0


class BasisValue(object):
  def __init__(self, price1, price2, basis_value):
    self.price1 = price1
    self.price2 = price2
    self.basis_value = basis_value
    # fair price2 = price1 - basis_value
    # fair price1 = price2 + basis_value

  def get_basis_add_to_1(self):
    return -self.basis_value

  def get_basis_add_to_2(self):
    return self.basis_value

  def get_price1_from_price2_and_basis(self):
    return self.get_price1_from_given_price2_and_basis(self.price2)

  def get_price2_from_price1_and_basis(self):
    return self.get_price2_from_given_price1_and_basis(self.price1)

  def get_price1_from_given_price2_and_basis(self, price2):
    return price2 + self.basis_value

  def get_price2_from_given_price1_and_basis(self, price1):
    return price1 - self.basis_value

  def basis_edge_sell1_buy2(self):
    return float(self.basis_value) / self.price2

  def basis_edge_buy1_sell2(self):
    return float(-self.basis_value) / self.price1


def get_basis_value(currency, exchange1, exchange2, basis_sec):
  basis_key = str(10 * basis_sec)
  json_value = get_basis_json()
  for reverse, exch_a, exch_b in [(False, exchange1, exchange2), (True, exchange2, exchange1)]:
    keya = "%s.%s.mid_price_1m" % (exch_a.upper(), currency.upper())
    keyb = "%s.%s.mid_price_1m" % (exch_b.upper(), currency.upper())
    key_name = "%s,%s" % (keya, keyb)
    if key_name in json_value['basis']:
      basis_obj = json_value['basis'][key_name]['basis'][basis_key]
      assert basis_obj['window_size_sec'] == basis_sec
      pricea = json_value['price'][keya]['price']
      priceb = json_value['price'][keyb]['price']
      if basis_obj['coverage'] > 0.25:
        if reverse:
          return BasisValue(priceb, pricea, -basis_obj['value'])
        else:
          return BasisValue(pricea, priceb, basis_obj['value'])
      else:
        raise ValueError("basis coverage is low", basis_obj)
  return None


def get_ref_times_from_info(info):
  if hasattr(info.exch1.feed, "exchange_time") and info.exch1.feed.exchange_time:
    ref_time1 = info.exch1.feed.exchange_time
  else:
    ref_time1 = info.exch1.feed.fetched_time

  if hasattr(info.exch2.feed, "exchange_time") and info.exch2.feed.exchange_time:
    ref_time2 = info.exch2.feed.exchange_time
  else:
    ref_time2 = info.exch2.feed.fetched_time

  return (ref_time1, ref_time2)


def get_post_buy_qty(info, exch, currency):
  exec_info = info.config
  exch_bal = hard_arb_general.get_currency_balance(info, exch, currency)
  ref_price = hard_arb_general.get_ref_price_mean([info.exch1, info.exch2], currency)

  lot_size = (exec_info["lot_size_krw"] / float(ref_price))
  qty = exec_info["max_position_map"][exch.exchange] - exch_bal.total
  qty = min(lot_size, qty)
  qty = hard_arb_general.truncate(qty)
  if qty < 0:
    return 0.0
  return float(qty)


def get_post_sell_qty(info, exch, currency):
  exec_info = info.config
  exch_bal = hard_arb_general.get_currency_balance(info, exch, currency)
  ref_price = hard_arb_general.get_ref_price_mean([info.exch1, info.exch2], currency)

  lot_size = (exec_info["lot_size_krw"] / float(ref_price))
  qty = exch_bal.total
  qty = min(lot_size, qty)
  qty = hard_arb_general.truncate(qty)
  return float(qty)


def check_expected_deposit(info, exch, currency):
  exch_bal = hard_arb_general.get_currency_balance(info, exch, currency)
  max_coin = info.config["max_position_map"][exch.exchange]
  print("Total:%s, Max: %s" % (exch_bal.total, max_coin))
  return exch_bal.total <= max_coin * 1.1


def is_expected_deposit(info, currency):
  if flags.FLAGS.trade_exchange1 and not check_expected_deposit(info, info.exch1, currency):
    return False
  if flags.FLAGS.trade_exchange2 and not check_expected_deposit(info, info.exch2, currency):
    return False
  return True


def round_up(price, tick_size):
  assert tick_size > 0.0000001
  return math.ceil(float(price) / tick_size) * tick_size


def round_down(price, tick_size):
  assert tick_size > 0.0000001
  return math.floor(float(price) / tick_size) * tick_size


def cancel_orders(exch, orders, exclude_price, stickiness, currency):
  for order in orders:
    order_price = int(order.price)
    print("Sticky price range: %s <= %s <= %s?" %
          (exclude_price * (1 - stickiness), order_price, exclude_price * (1 + stickiness)))
    if abs(order_price - exclude_price) > exclude_price * stickiness:
      print("Trying to cancel an order")
      print("  Price: %d" % order_price)
      print("  Side: %s" % order.side)
      exch.order.query_cancel(exch.order.get_product(currency), order.order_id)


def post_excha_sell(info, excha, exchb, currency, ask_orders, tick_size):
  exec_info = info.config

  excha_price = excha.feed.get_price_by_notional(exec_info["price_det_notional"])
  exchb_price = exchb.feed.get_price_by_notional(exec_info["price_det_notional"])

  basis_value = get_basis_value(currency, excha.exchange, exchb.exchange, exec_info["basis_sec"])

  target_fair_ask1 = basis_value.get_price1_from_given_price2_and_basis(exchb_price.ask_price)

  if flags.FLAGS.passive_only:
    target_fair_ask1 = max(target_fair_ask1, excha_price.bid_price)

  target_ask_price = target_fair_ask1 * (1 + exec_info["posting_margin"])

  print("(cross)%s ask: %s" % (exchb.exchange, exchb_price.ask_price))
  print("target fair ask%s: %s" % (excha.exchange, target_fair_ask1))

  print("target ask from margin: %s" % target_ask_price)

  sell_price = round_up(target_ask_price, exec_info["tick_size_map"][excha.exchange])
  sell_qty = get_post_sell_qty(info, excha, currency)
  if sell_qty == 0:
    return
  exch1_bal = hard_arb_general.get_currency_balance(info, excha, currency)

  if not ask_orders:
    if sell_qty >= exec_info["min_order_size"] and \
       hard_arb_general.has_enough_bal(exch1_bal, sell_qty):
      print("Trying to send a sell order")
      print("  Price: %d" % sell_price)
      print("  Qty: %f" % sell_qty)
      excha.order.query_submit(excha.order.get_product(currency), "ask", sell_price, sell_qty)
  else:
    cancel_orders(excha, ask_orders, sell_price, exec_info["stickiness"], currency)


def post_excha_buy(info, excha, exchb, currency, bid_orders, tick_size):
  exec_info = info.config

  excha_price = excha.feed.get_price_by_notional(exec_info["price_det_notional"])
  exchb_price = exchb.feed.get_price_by_notional(exec_info["price_det_notional"])

  basis_value = get_basis_value(currency, excha.exchange, exchb.exchange, exec_info["basis_sec"])
  target_fair_bid1 = basis_value.get_price1_from_given_price2_and_basis(exchb_price.bid_price)

  if flags.FLAGS.passive_only:
    target_fair_bid1 = min(target_fair_bid1, excha_price.ask_price)

  target_bid_price = target_fair_bid1 * (1 - exec_info["posting_margin"])

  print("(cross)%s bid: %s" % (exchb.exchange, exchb_price.bid_price))
  print("target fair bid%s: %s" % (excha.exchange, target_fair_bid1))
  print("target bid from margin: %s" % target_bid_price)

  buy_price = round_down(target_bid_price, exec_info["tick_size_map"][excha.exchange])
  buy_qty = get_post_buy_qty(info, excha, currency)
  if buy_qty == 0:
    return
  exch1_krw = hard_arb_general.get_currency_balance(info, excha, "KRW")

  if not bid_orders:
    if buy_qty >= exec_info["min_order_size"] and \
       hard_arb_general.has_enough_bal(exch1_krw, buy_qty * buy_price):
      print("Trying to send a buy order")
      print("  Price: %d" % buy_price)
      print("  Qty: %f" % buy_qty)
      excha.order.query_submit(excha.order.get_product(currency), "bid", buy_price, buy_qty)
  else:
    cancel_orders(excha, bid_orders, buy_price, exec_info["stickiness"], currency)


def query_sided_orders(exch, currency):
  update = exch.order.query_list_orders(exch.order.get_product(currency))
  ask_orders = []
  bid_orders = []
  for order in update.msg.each_order:
    if order.side == coin_order.SELL_ORDER:
      ask_orders.append(order)
    elif order.side == coin_order.BUY_ORDER:
      bid_orders.append(order)
    else:
      raise ValueError(order.side)
  return {"ask": ask_orders, "bid": bid_orders}


def post_exch1(info, currency, tick_size=1):
  if not flags.FLAGS.trade_exchange1:
    print("not trading %s" % flags.FLAGS.exchange1)
    return

  ref_time1, ref_time2 = get_ref_times_from_info(info)
  diff_time = abs(ref_time1 - ref_time2)
  if (diff_time > 5 * 1e+9):
    return

  orders = query_sided_orders(info.exch1, currency)
  ask_orders = orders["ask"]
  bid_orders = orders["bid"]
  try:
    post_excha_buy(info, info.exch1, info.exch2, currency, bid_orders, tick_size)
  except Exception:
    traceback.print_exc()
  time.sleep(0.1)
  try:
    post_excha_sell(info, info.exch1, info.exch2, currency, ask_orders, tick_size)
  except Exception:
    traceback.print_exc()


def post_exch2(info, currency, tick_size=1):
  if not flags.FLAGS.trade_exchange2:
    print("not trading %s" % flags.FLAGS.exchange2)
    return

  ref_time1, ref_time2 = get_ref_times_from_info(info)
  diff_time = abs(ref_time1 - ref_time2)
  if (diff_time > 5 * 1e+9):
    return

  orders = query_sided_orders(info.exch2, currency)
  ask_orders = orders["ask"]
  bid_orders = orders["bid"]
  try:
    post_excha_buy(info, info.exch2, info.exch1, currency, bid_orders, tick_size)
  except Exception:
    traceback.print_exc()
  time.sleep(0.1)
  try:
    post_excha_sell(info, info.exch2, info.exch1, currency, ask_orders, tick_size)
  except Exception:
    traceback.print_exc()


def trade_mm(info, currency):
  try:
    hard_arb_general.execute_thread_runners([
        hard_arb_general.ThreadRunner(info.exch1.feed.query_book),
        hard_arb_general.ThreadRunner(info.exch2.feed.query_book),
        hard_arb_general.ThreadRunner(info.exch1.order.query_balance),
        hard_arb_general.ThreadRunner(info.exch2.order.query_balance)
    ],
                                            "Query balance and market data...")
  except Exception:
    # Try next time
    traceback.print_exc()
    return

  if not is_expected_deposit(info, currency):
    print
    print("It seems that %s deposit is quite different "
          "from expectation. Skip trading..." % currency)
    return

  ref_time1, ref_time2 = get_ref_times_from_info(info)
  diff_time = abs(ref_time1 - ref_time2)
  if (diff_time > 5 * 1e+9):
    print("tdiff=%s, %s time1 = %s, %s time2 = %s" %
          (diff_time, info.exch1.exchange, ref_time1, info.exch2.exchange, ref_time2))
    if diff_time > 30 * 1e+9:
      die_flag = True
      raise ValueError("diff is serious")
    return
  else:
    print("tdiff=%s, normal" % diff_time)

  try:
    hard_arb_general.execute_thread_runners([
        hard_arb_general.ThreadRunner(post_exch1, info, currency),
        hard_arb_general.ThreadRunner(post_exch2, info, currency)
    ],
                                            "Manage orders...")
  except Exception:
    traceback.print_exc()


def trade(info, currency):
  try:
    trade_mm(info, currency)
  except KeyboardInterrupt:
    raise
  except Exception as exc:
    print(exc)
    print("A problem detected. Try again")
    traceback.print_exc()


def main(_):
  currency = flags.FLAGS.currency
  info = create_arb_info(currency,
                         flags.FLAGS.exchange1,
                         flags.FLAGS.exchange2,
                         flags.FLAGS.key_file1,
                         flags.FLAGS.key_file2, {}, {},
                         flags.FLAGS.config_filename)

  hard_arb_general.execute_thread_runners([
      hard_arb_general.ThreadRunner(info.exch1.feed.query_book),
      hard_arb_general.ThreadRunner(info.exch2.feed.query_book),
      hard_arb_general.ThreadRunner(info.exch1.order.query_balance),
      hard_arb_general.ThreadRunner(info.exch2.order.query_balance)
  ],
                                          "Init Data")

  while (True):
    time.sleep(flags.FLAGS.sleep_time)
    print()
    print(datetime.datetime.now())
    print("Checking...")
    trade(info, currency)
    if die_flag:
      break
  return 0


if __name__ == '__main__':
  app.run(main)
